/*
 * Copyright 2013, Edmodo, Inc. 
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License.
 * You may obtain a copy of the License in the LICENSE file, or at:
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" 
 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language 
 * governing permissions and limitations under the License. 
 */

package com.smartandroid.sa.cropper;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.media.ExifInterface;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;

/**
 * Custom view that provides cropping capabilities to an image.
 */
public class CropImageView extends FrameLayout {

	// Private Constants ///////////////////////////////////////////////////////

	private static final Rect EMPTY_RECT = new Rect();

	// Member Variables ////////////////////////////////////////////////////////

	// Sets the default image guidelines to show when resizing
	public static final int DEFAULT_GUIDELINES = 1;
	public static final boolean DEFAULT_FIXED_ASPECT_RATIO = false;
	public static final int DEFAULT_ASPECT_RATIO_X = 1;
	public static final int DEFAULT_ASPECT_RATIO_Y = 1;

	private static final int DEFAULT_IMAGE_RESOURCE = 0;

	private static final String DEGREES_ROTATED = "DEGREES_ROTATED";

	private ImageView mImageView;
	private CropOverlayView mCropOverlayView;

	private Bitmap mBitmap;
	private int mDegreesRotated = 0;

	private int mLayoutWidth;
	private int mLayoutHeight;

	// Instance variables for customizable attributes
	private int mGuidelines = DEFAULT_GUIDELINES;
	private boolean mFixAspectRatio = DEFAULT_FIXED_ASPECT_RATIO;
	private int mAspectRatioX = DEFAULT_ASPECT_RATIO_X;
	private int mAspectRatioY = DEFAULT_ASPECT_RATIO_Y;
	private int mImageResource = DEFAULT_IMAGE_RESOURCE;
	public static final int[] CropImageView = { 0x7f010000, 0x7f010001,
			0x7f010002, 0x7f010003, 0x7f010004 };

	// Constructors ////////////////////////////////////////////////////////////

	public CropImageView(Context context) {
		super(context);
		init(context);
	}

	public CropImageView(Context context, AttributeSet attrs) {
		super(context, attrs);

		TypedArray ta = context.obtainStyledAttributes(attrs, CropImageView, 0,
				0);
		//
		// TypedArray ta = context.obtainStyledAttributes(attrs,
		// R.styleable.CropImageView, 0, 0);

		try {
			mGuidelines = ta.getInteger(0, DEFAULT_GUIDELINES);
			// mGuidelines = ta.getInteger(R.styleable.CropImageView_guidelines,
			// DEFAULT_GUIDELINES);
			mFixAspectRatio = ta.getBoolean(1, DEFAULT_FIXED_ASPECT_RATIO);
			// mFixAspectRatio = ta.getBoolean(
			// R.styleable.CropImageView_fixAspectRatio,
			// DEFAULT_FIXED_ASPECT_RATIO);
			mAspectRatioX = ta.getInteger(2, DEFAULT_ASPECT_RATIO_X);
			// mAspectRatioX = ta.getInteger(
			// R.styleable.CropImageView_aspectRatioX,
			// DEFAULT_ASPECT_RATIO_X);
			mAspectRatioY = ta.getInteger(3, DEFAULT_ASPECT_RATIO_Y);
			// mAspectRatioY = ta.getInteger(
			// R.styleable.CropImageView_aspectRatioY,
			// DEFAULT_ASPECT_RATIO_Y);
			mImageResource = ta.getResourceId(4, DEFAULT_IMAGE_RESOURCE);
			// mImageResource = ta.getResourceId(
			// R.styleable.CropImageView_imageResource,
			// DEFAULT_IMAGE_RESOURCE);
		} finally {
			ta.recycle();
		}

		init(context);
	}

	// View Methods ////////////////////////////////////////////////////////////

	@Override
	public Parcelable onSaveInstanceState() {

		final Bundle bundle = new Bundle();

		bundle.putParcelable("instanceState", super.onSaveInstanceState());
		bundle.putInt(DEGREES_ROTATED, mDegreesRotated);

		return bundle;

	}

	@Override
	public void onRestoreInstanceState(Parcelable state) {

		if (state instanceof Bundle) {

			final Bundle bundle = (Bundle) state;

			if (mBitmap != null) {
				// Fixes the rotation of the image when orientation changes.
				mDegreesRotated = bundle.getInt(DEGREES_ROTATED);
				int tempDegrees = mDegreesRotated;
				rotateImage(mDegreesRotated);
				mDegreesRotated = tempDegrees;
			}

			super.onRestoreInstanceState(bundle.getParcelable("instanceState"));

		} else {
			super.onRestoreInstanceState(state);
		}
	}

	@Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {

		if (mBitmap != null) {
			final Rect bitmapRect = ImageViewUtil.getBitmapRectCenterInside(
					mBitmap, this);
			mCropOverlayView.setBitmapRect(bitmapRect);
		} else {
			mCropOverlayView.setBitmapRect(EMPTY_RECT);
		}
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

		int widthMode = MeasureSpec.getMode(widthMeasureSpec);
		int widthSize = MeasureSpec.getSize(widthMeasureSpec);
		int heightMode = MeasureSpec.getMode(heightMeasureSpec);
		int heightSize = MeasureSpec.getSize(heightMeasureSpec);

		if (mBitmap != null) {

			super.onMeasure(widthMeasureSpec, heightMeasureSpec);

			// Bypasses a baffling bug when used within a ScrollView, where
			// heightSize is set to 0.
			if (heightSize == 0)
				heightSize = mBitmap.getHeight();

			int desiredWidth;
			int desiredHeight;

			double viewToBitmapWidthRatio = Double.POSITIVE_INFINITY;
			double viewToBitmapHeightRatio = Double.POSITIVE_INFINITY;

			// Checks if either width or height needs to be fixed
			if (widthSize < mBitmap.getWidth()) {
				viewToBitmapWidthRatio = (double) widthSize
						/ (double) mBitmap.getWidth();
			}
			if (heightSize < mBitmap.getHeight()) {
				viewToBitmapHeightRatio = (double) heightSize
						/ (double) mBitmap.getHeight();
			}

			// If either needs to be fixed, choose smallest ratio and calculate
			// from there
			if (viewToBitmapWidthRatio != Double.POSITIVE_INFINITY
					|| viewToBitmapHeightRatio != Double.POSITIVE_INFINITY) {
				if (viewToBitmapWidthRatio <= viewToBitmapHeightRatio) {
					desiredWidth = widthSize;
					desiredHeight = (int) (mBitmap.getHeight() * viewToBitmapWidthRatio);
				} else {
					desiredHeight = heightSize;
					desiredWidth = (int) (mBitmap.getWidth() * viewToBitmapHeightRatio);
				}
			}

			// Otherwise, the picture is within frame layout bounds. Desired
			// width is
			// simply picture size
			else {
				desiredWidth = mBitmap.getWidth();
				desiredHeight = mBitmap.getHeight();
			}

			int width = getOnMeasureSpec(widthMode, widthSize, desiredWidth);
			int height = getOnMeasureSpec(heightMode, heightSize, desiredHeight);

			mLayoutWidth = width;
			mLayoutHeight = height;

			final Rect bitmapRect = ImageViewUtil.getBitmapRectCenterInside(
					mBitmap.getWidth(), mBitmap.getHeight(), mLayoutWidth,
					mLayoutHeight);
			mCropOverlayView.setBitmapRect(bitmapRect);

			// MUST CALL THIS
			setMeasuredDimension(mLayoutWidth, mLayoutHeight);

		} else {

			mCropOverlayView.setBitmapRect(EMPTY_RECT);
			setMeasuredDimension(widthSize, heightSize);
		}
	}

	protected void onLayout(boolean changed, int l, int t, int r, int b) {

		super.onLayout(changed, l, t, r, b);

		if (mLayoutWidth > 0 && mLayoutHeight > 0) {
			// Gets original parameters, and creates the new parameters
			final ViewGroup.LayoutParams origparams = this.getLayoutParams();
			origparams.width = mLayoutWidth;
			origparams.height = mLayoutHeight;
			setLayoutParams(origparams);
		}
	}

	// Public Methods //////////////////////////////////////////////////////////

	/**
	 * Returns the integer of the imageResource
	 * 
	 * @param int the image resource id
	 */
	public int getImageResource() {
		return mImageResource;
	}

	/**
	 * Sets a Bitmap as the content of the CropImageView.
	 * 
	 * @param bitmap
	 *            the Bitmap to set
	 */
	public void setImageBitmap(Bitmap bitmap) {

		mBitmap = bitmap;
		mImageView.setImageBitmap(mBitmap);

		if (mCropOverlayView != null) {
			mCropOverlayView.resetCropOverlayView();
		}
	}

	/**
	 * Sets a Bitmap and initializes the image rotation according to the EXIT
	 * data.
	 * <p>
	 * The EXIF can be retrieved by doing the following:
	 * <code>ExifInterface exif = new ExifInterface(path);</code>
	 * 
	 * @param bitmap
	 *            the original bitmap to set; if null, this
	 * @param exif
	 *            the EXIF information about this bitmap; may be null
	 */
	public void setImageBitmap(Bitmap bitmap, ExifInterface exif) {

		if (bitmap == null) {
			return;
		}

		if (exif == null) {
			setImageBitmap(bitmap);
			return;
		}

		final Matrix matrix = new Matrix();
		final int orientation = exif.getAttributeInt(
				ExifInterface.TAG_ORIENTATION, 1);
		int rotate = -1;

		switch (orientation) {
		case ExifInterface.ORIENTATION_ROTATE_270:
			rotate = 270;
			break;
		case ExifInterface.ORIENTATION_ROTATE_180:
			rotate = 180;
			break;
		case ExifInterface.ORIENTATION_ROTATE_90:
			rotate = 90;
			break;
		}

		if (rotate == -1) {
			setImageBitmap(bitmap);
		} else {
			matrix.postRotate(rotate);
			final Bitmap rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0,
					bitmap.getWidth(), bitmap.getHeight(), matrix, true);
			setImageBitmap(rotatedBitmap);
			bitmap.recycle();
		}
	}

	/**
	 * Sets a Drawable as the content of the CropImageView.
	 * 
	 * @param resId
	 *            the drawable resource ID to set
	 */
	public void setImageResource(int resId) {
		if (resId != 0) {
			Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId);
			setImageBitmap(bitmap);
		}
	}

	/**
	 * Gets the cropped image based on the current crop window.
	 * 
	 * @return a new Bitmap representing the cropped image
	 */
	public Bitmap getCroppedImage() {

		final Rect displayedImageRect = ImageViewUtil
				.getBitmapRectCenterInside(mBitmap, mImageView);

		// Get the scale factor between the actual Bitmap dimensions and the
		// displayed dimensions for width.
		final float actualImageWidth = mBitmap.getWidth();
		final float displayedImageWidth = displayedImageRect.width();
		final float scaleFactorWidth = actualImageWidth / displayedImageWidth;

		// Get the scale factor between the actual Bitmap dimensions and the
		// displayed dimensions for height.
		final float actualImageHeight = mBitmap.getHeight();
		final float displayedImageHeight = displayedImageRect.height();
		final float scaleFactorHeight = actualImageHeight
				/ displayedImageHeight;

		// Get crop window position relative to the displayed image.
		final float cropWindowX = Edge.LEFT.getCoordinate()
				- displayedImageRect.left;
		final float cropWindowY = Edge.TOP.getCoordinate()
				- displayedImageRect.top;
		final float cropWindowWidth = Edge.getWidth();
		final float cropWindowHeight = Edge.getHeight();

		// Scale the crop window position to the actual size of the Bitmap.
		final float actualCropX = cropWindowX * scaleFactorWidth;
		final float actualCropY = cropWindowY * scaleFactorHeight;
		final float actualCropWidth = cropWindowWidth * scaleFactorWidth;
		final float actualCropHeight = cropWindowHeight * scaleFactorHeight;

		// Crop the subset from the original Bitmap.
		final Bitmap croppedBitmap = Bitmap.createBitmap(mBitmap,
				(int) actualCropX, (int) actualCropY, (int) actualCropWidth,
				(int) actualCropHeight);

		return croppedBitmap;
	}

	/**
	 * Gets the crop window's position relative to the source Bitmap (not the
	 * image displayed in the CropImageView).
	 * 
	 * @return a RectF instance containing cropped area boundaries of the source
	 *         Bitmap
	 */
	public RectF getActualCropRect() {

		final Rect displayedImageRect = ImageViewUtil
				.getBitmapRectCenterInside(mBitmap, mImageView);

		// Get the scale factor between the actual Bitmap dimensions and the
		// displayed dimensions for width.
		final float actualImageWidth = mBitmap.getWidth();
		final float displayedImageWidth = displayedImageRect.width();
		final float scaleFactorWidth = actualImageWidth / displayedImageWidth;

		// Get the scale factor between the actual Bitmap dimensions and the
		// displayed dimensions for height.
		final float actualImageHeight = mBitmap.getHeight();
		final float displayedImageHeight = displayedImageRect.height();
		final float scaleFactorHeight = actualImageHeight
				/ displayedImageHeight;

		// Get crop window position relative to the displayed image.
		final float displayedCropLeft = Edge.LEFT.getCoordinate()
				- displayedImageRect.left;
		final float displayedCropTop = Edge.TOP.getCoordinate()
				- displayedImageRect.top;
		final float displayedCropWidth = Edge.getWidth();
		final float displayedCropHeight = Edge.getHeight();

		// Scale the crop window position to the actual size of the Bitmap.
		float actualCropLeft = displayedCropLeft * scaleFactorWidth;
		float actualCropTop = displayedCropTop * scaleFactorHeight;
		float actualCropRight = actualCropLeft + displayedCropWidth
				* scaleFactorWidth;
		float actualCropBottom = actualCropTop + displayedCropHeight
				* scaleFactorHeight;

		// Correct for floating point errors. Crop rect boundaries should not
		// exceed the source Bitmap bounds.
		actualCropLeft = Math.max(0f, actualCropLeft);
		actualCropTop = Math.max(0f, actualCropTop);
		actualCropRight = Math.min(mBitmap.getWidth(), actualCropRight);
		actualCropBottom = Math.min(mBitmap.getHeight(), actualCropBottom);

		final RectF actualCropRect = new RectF(actualCropLeft, actualCropTop,
				actualCropRight, actualCropBottom);

		return actualCropRect;
	}

	/**
	 * Sets whether the aspect ratio is fixed or not; true fixes the aspect
	 * ratio, while false allows it to be changed.
	 * 
	 * @param fixAspectRatio
	 *            Boolean that signals whether the aspect ratio should be
	 *            maintained.
	 */
	public void setFixedAspectRatio(boolean fixAspectRatio) {
		mCropOverlayView.setFixedAspectRatio(fixAspectRatio);
	}

	/**
	 * Sets the guidelines for the CropOverlayView to be either on, off, or to
	 * show when resizing the application.
	 * 
	 * @param guidelines
	 *            Integer that signals whether the guidelines should be on, off,
	 *            or only showing when resizing.
	 */
	public void setGuidelines(int guidelines) {
		mCropOverlayView.setGuidelines(guidelines);
	}

	/**
	 * Sets the both the X and Y values of the aspectRatio.
	 * 
	 * @param aspectRatioX
	 *            int that specifies the new X value of the aspect ratio
	 * @param aspectRatioX
	 *            int that specifies the new Y value of the aspect ratio
	 */
	public void setAspectRatio(int aspectRatioX, int aspectRatioY) {
		mAspectRatioX = aspectRatioX;
		mCropOverlayView.setAspectRatioX(mAspectRatioX);

		mAspectRatioY = aspectRatioY;
		mCropOverlayView.setAspectRatioY(mAspectRatioY);
	}

	/**
	 * Rotates image by the specified number of degrees clockwise. Cycles from 0
	 * to 360 degrees.
	 * 
	 * @param degrees
	 *            Integer specifying the number of degrees to rotate.
	 */
	public void rotateImage(int degrees) {

		Matrix matrix = new Matrix();
		matrix.postRotate(degrees);
		mBitmap = Bitmap.createBitmap(mBitmap, 0, 0, mBitmap.getWidth(),
				mBitmap.getHeight(), matrix, true);
		setImageBitmap(mBitmap);

		mDegreesRotated += degrees;
		mDegreesRotated = mDegreesRotated % 360;
	}

	// Private Methods /////////////////////////////////////////////////////////

	private void init(Context context) {

		final LayoutInflater inflater = LayoutInflater.from(context);
		final View v = inflater.inflate(
				getResources().getIdentifier("crop_image_view", "layout",
						context.getPackageName()), this, true);
		// final View v = inflater.inflate(R.layout.crop_image_view, this,
		// true);

		mImageView = (ImageView) v.findViewById(getResources().getIdentifier(
				"ImageView_image", "id", context.getPackageName()));
		// mImageView = (ImageView) v.findViewById(R.id.ImageView_image);

		setImageResource(mImageResource);
		mCropOverlayView = (CropOverlayView) v.findViewById(getResources()
				.getIdentifier("CropOverlayView", "id",
						context.getPackageName()));
		// mCropOverlayView = (CropOverlayView) v
		// .findViewById(R.id.CropOverlayView);
		mCropOverlayView.setInitialAttributeValues(mGuidelines,
				mFixAspectRatio, mAspectRatioX, mAspectRatioY);
	}

	/**
	 * Determines the specs for the onMeasure function. Calculates the width or
	 * height depending on the mode.
	 * 
	 * @param measureSpecMode
	 *            The mode of the measured width or height.
	 * @param measureSpecSize
	 *            The size of the measured width or height.
	 * @param desiredSize
	 *            The desired size of the measured width or height.
	 * @return The final size of the width or height.
	 */
	private static int getOnMeasureSpec(int measureSpecMode,
			int measureSpecSize, int desiredSize) {

		// Measure Width
		int spec;
		if (measureSpecMode == MeasureSpec.EXACTLY) {
			// Must be this size
			spec = measureSpecSize;
		} else if (measureSpecMode == MeasureSpec.AT_MOST) {
			// Can't be bigger than...; match_parent value
			spec = Math.min(desiredSize, measureSpecSize);
		} else {
			// Be whatever you want; wrap_content
			spec = desiredSize;
		}

		return spec;
	}

}
